// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: xguo, taekwon

#pragma once

#include <map>
#include <memory>
#include <set>
#include <string>

#include "coin2/exchange/base/order/account_info.h"
#include "coin2/exchange/base/order/external.h"
#include "coin2/exchange/base/order/order_context_manager.h"

namespace coin2::exchange::base::order {

// This class corresponds to BookBuilder
class ProductOrderInfo {
 public:
  ProductOrderInfo() = default;

  void set_event(const OrderEvent* event) { event_ = *event; }

  const OrderEvent& event() const { return event_; }

  const auto& order_contexts() const { return order_contexts_; }

  double fill_pos() const { return fill_pos_; }

  const OrderContext* GetCurrentOrderContexts() const {
    CHECK(event_.has_proc_order_id());
    return order_contexts_.at(event_.proc_order_id());
  }

  void UpdateOrderState(const OrderEvent& order_event, const OrderContext* order_context) {
    using coin::proto::TradeSide;
    CHECK_EQ(order_event.proc_order_id(), order_context->proc_order_id());
    event_ = order_event;
    int64_t proc_order_id = order_context->proc_order_id();
    auto it = order_contexts_.find(proc_order_id);
    const auto type = order_event.type();
    const bool is_dead = (type == OrderEvent::CANCEL_CONFIRMED) ||
                         (type == OrderEvent::ORDER_REJECTED) || order_event.fully_filled();

    if (is_dead && it != order_contexts_.end()) {
      order_contexts_.erase(it);
    } else if (!is_dead && it == order_contexts_.end()) {
      order_contexts_[proc_order_id] = order_context;
    }

    if (type == OrderEvent::ORDER_FILLED) {
      if (order_context->order_created_time() > fill_pos_init_ts_) {
        if (order_event.trade_side() == TradeSide::TRADE_BUY_SIDE) {
          fill_pos_ += order_event.fill_qty();
        } else if (order_event.trade_side() == TradeSide::TRADE_SELL_SIDE) {
          fill_pos_ -= order_event.fill_qty();
        }
      }
    }
  }

  void SetInitFillPosition(double total_pos, int64_t ts) {
    CHECK_EQ(fill_pos_init_ts_, 0);
    fill_pos_ = total_pos;
    fill_pos_init_ts_ = ts;
  }

 protected:
  OrderEvent event_;
  // internal_order_id => context
  std::map<int64_t, const OrderContext*> order_contexts_;
  double fill_pos_;
  int64_t fill_pos_init_ts_;
};

class OrderUpdate : public TopicRecordUpdate {
 public:
  // We need MEA instead of ME because api_version is required to understand native_symbol.
  explicit OrderUpdate(const std::string& system_name, const MarketExchangeApi& mea)
      : system_name_(system_name), mea_(mea), product_{nullptr} {}
  ~OrderUpdate() override { Reset(); }

  const std::string& system_name() const { return system_name_; }
  const MarketExchangeApi& mea() const { return mea_; }

  OrderUpdateType update_type() const { return type_; }

  bool is_account_info() const { return type_ == coin::proto::ACCOUNT_INFO_UPDATE_TYPE; }

  bool is_product_order_info() const { return type_ == coin::proto::PRODUCT_INFO_UPDATE_TYPE; }

  const std::string& native_symbol() const { return native_symbol_; }
  const IProduct& product() const { return *product_; }

  const AccountInfo& account_info() const {
    CHECK(account_info_ != nullptr);
    return *account_info_;
  }
  const ProductOrderInfo& product_order_info() const {
    CHECK(product_order_info_ != nullptr);
    return *product_order_info_;
  }

  std::optional<int64_t> sim_account_id;

 public:
  void Reset() override {
    ResetTopicRecordUpdate();
    ResetOrderUpdate();
  }

  void ResetOrderUpdate() {
    type_ = coin::proto::UNKNOWN_ORDER_UPDATE_TYPE;
    native_symbol_ = "";
    product_ = nullptr;
  }

  void SetAccountInfo(AccountInfo* account_info) {
    SetOrderUpdateType(coin::proto::ACCOUNT_INFO_UPDATE_TYPE);
    native_symbol_ = "";
    account_info_ = account_info;
    product_order_info_ = nullptr;
  }

  void SetProductOrderInfo(const std::string& native, ProductOrderInfo* product_order_info) {
    CHECK(!native.empty());
    CHECK(product_order_info != nullptr);

    SetOrderUpdateType(coin::proto::PRODUCT_INFO_UPDATE_TYPE);
    native_symbol_ = native;
    account_info_ = nullptr;
    product_order_info_ = product_order_info;
    if (native_map_.count(native) > 0) {
      product_ = native_map_.at(native);
    } else {
      LOG(INFO) << "native symbol not found: " << native << ". please ignore this for conversion";
      if (native_map_.size() > 0) {
        LOG(INFO) << " did you mean " << native_map_.begin()->first;
      }
    }
  }

  void AddProduct(const IProduct* product) {
    native_map_.emplace(product->order_native_symbol(), product);
  }

  void SetOrderUpdateType(const OrderUpdateType& type) { type_ = type; }

 protected:
  // must be const
  const std::string system_name_;
  const MarketExchangeApi mea_;

  OrderUpdateType type_{OrderUpdateType::UNKNOWN_ORDER_UPDATE_TYPE};
  std::string native_symbol_;

  // borrow from OrderUpdateManager
  const IProduct* product_{nullptr};
  AccountInfo* account_info_{nullptr};
  ProductOrderInfo* product_order_info_{nullptr};
  std::map<std::string, const IProduct*> native_map_;

 private:
  OrderUpdate(const OrderUpdate&) = delete;
  OrderUpdate& operator=(const OrderUpdate&) = delete;
};

class IOrderSubscriber {
 public:
  virtual void onAccountInfo(const OrderUpdate& upd) = 0;
  virtual void onAccountOrder(const OrderUpdate& upd) = 0;
  virtual void onOrderLog(const OrderGatewayLog& log) = 0;
  virtual void onOrderSubsystemError(const OrderUpdate& upd) {}
  virtual ~IOrderSubscriber() = default;
};

class OrderDistributor {
 public:
  const std::set<std::string>& native_symbols() const { return native_symbols_; }

  bool is_symbol_registered(const std::string& native_symbol) const {
    return native_symbols_.count(native_symbol) > 0;
  }

  void AddNativeSymbol(const std::string& native_symbol, IOrderSubscriber* subscriber) {
    CHECK(!native_symbol.empty()) << native_symbol;
    CHECK(subscriber) << subscriber;
    CHECK_EQ(native_symbols_.count(native_symbol), 0) << native_symbol;
    CHECK_EQ(subscriber_map_.count(native_symbol), 0) << native_symbol;
    native_symbols_.insert(native_symbol);
    subscriber_map_[native_symbol] = subscriber;
    if (subscriber_set_.count(subscriber) == 0) {
      subscriber_set_.insert(subscriber);
    }
  }

  void onAccountInfo(const OrderUpdate& upd) {
    for (auto& it : subscriber_set_) {
      it->onAccountInfo(upd);
    }
  }

  void onAccountOrder(const std::string& native_symbol, const OrderUpdate& upd) {
    for (auto& it : subscriber_set_) {
      it->onAccountOrder(upd);
    }
  }

 private:
  std::set<std::string> native_symbols_;
  std::map<std::string, IOrderSubscriber*> subscriber_map_;
  std::set<IOrderSubscriber*> subscriber_set_;
};

// using namespace coin2::exchange;

class OrderUpdateManager {
 public:
  explicit OrderUpdateManager(const std::string& system_name, const MarketExchangeApi& mea)
      : packet_subscriber_(nullptr),
        order_distributor_(),
        upd_(system_name, mea),
        account_info_(mea) {}
  virtual ~OrderUpdateManager() = default;

  const OrderUpdate& update() const { return upd_; }
  const MarketExchangeApi& mea() const { return upd_.mea(); }

  const std::set<std::string>& native_symbols() const {
    return order_distributor_.native_symbols();
  }

  bool is_symbol_registered(const std::string& native_symbol) const {
    return order_distributor_.is_symbol_registered(native_symbol);
  }

  void AddNativeSymbol(const std::string& native_symbol, IOrderSubscriber* subscriber) {
    order_distributor_.AddNativeSymbol(native_symbol, subscriber);
    product_infos_[native_symbol] = std::make_unique<ProductOrderInfo>();
  }

  void AddProduct(const IProduct* product) { upd_.AddProduct(product); }

  void SetAllZeroPosition() { account_info_.SetAllZeroPosition(); }
  void SetAllZeroBalance() { account_info_.SetAllZeroBalance(); }

  double GetFillPosition(const std::string& native_symbol) const {
    return product_infos_.at(native_symbol)->fill_pos();
  }

  void SetOrderTimeout() { upd_.SetOrderUpdateType(coin::proto::OG_REST_TIMEOUT_ERROR_TYPE); }

 public:  // distributor
  virtual void onBeginPacket(const executor::TopicRecordPacket& packet) {
    upd_.Reset();
    upd_.SetTimestamp(packet.timestamp);
    if (packet_subscriber_ != nullptr) {
      packet_subscriber_->onBeginPacket(upd_);
    }
  }
  virtual void onEndPacket(const executor::TopicRecordPacket& /*packet*/) {
    if (packet_subscriber_ != nullptr) {
      packet_subscriber_->onEndPacket(upd_);
    }
  }

  const AccountInfo& account_info() const { return account_info_; }

  AccountInfo* GetAccountInfoMutable() {
    upd_.SetAccountInfo(&account_info_);
    return &account_info_;
  }

  OrderUpdate* GetOrderUpdateMutable() { return &upd_; }

  ProductOrderInfo* GetProductOrderInfoMutable(const std::string& native_symbol) {
    if (product_infos_.count(native_symbol) == 0) {
      product_infos_[native_symbol] = std::make_unique<ProductOrderInfo>();
    }
    auto* pi = product_infos_[native_symbol].get();
    CHECK(pi);
    upd_.SetProductOrderInfo(native_symbol, pi);
    return pi;
  }

  void UpdateAccountBalance(
      const coin::proto::AccountBalance& acc_balance,
      int64_t ts,
      bool all = false) {
    upd_.SetAccountInfo(&account_info_);
    account_info_.UpdateAccountBalance(acc_balance, all);
    if (mea().market == MarketType::Spot && ocm_) {
      for (const auto& bal : acc_balance.each_balance()) {
        auto native_symbol = bal.currency_native();
        ocm_->ClearFillNotSyncedPosFromCurrency(native_symbol);
        if (fill_pos_init_ts_map_.count(native_symbol) == 0) {
          fill_pos_init_ts_map_[native_symbol] = ts;
          // temporary disable this line
          // product_infos_[native_symbol]->SetInitFillPosition(bal.total());
        }
      }
    }
    PublishAccountInfo();
  }

  void UpdateAccountPosition(
      const coin::proto::AccountPosition& acc_position,
      int64_t ts,
      bool all = false) {
    upd_.SetAccountInfo(&account_info_);
    account_info_.UpdateAccountPosition(acc_position, all);
    if (ocm_) {
      for (const auto& pos : acc_position.each_position()) {
        auto native_symbol = pos.native_symbol();
        ocm_->ClearFillNotSyncedPos(native_symbol);
        if (product_infos_.count(native_symbol) > 0 &&
            fill_pos_init_ts_map_.count(native_symbol) == 0) {
          fill_pos_init_ts_map_[native_symbol] = ts;
          product_infos_[native_symbol]->SetInitFillPosition(pos.net_position(), ts);
        }
      }
    }
    PublishAccountInfo();
  }

  void UpdateCurrencyTransfer(
      const coin::proto::CurrencyTransferBundle& ccy_transfer_bundle,
      int64_t ts) {
    upd_.SetAccountInfo(&account_info_);
    account_info_.UpdateCurrencyTransfer(ccy_transfer_bundle);
    PublishAccountInfo();
  }

  void UpdateFundingFee(const coin::proto::FundingFeeBundle& funding_fee, int64_t ts) {
    upd_.SetAccountInfo(&account_info_);
    account_info_.UpdateFundingFee(funding_fee);
    PublishAccountInfo();
  }

  void UpdateNftTokenInfo(const ::coin::proto::NftTokenInfo& nft_token_info, int64_t ts) {
    upd_.SetAccountInfo(&account_info_);
    account_info_.UpdateNftTokenInfo(nft_token_info);
    PublishAccountInfo();
  }

  void PublishAccountInfo() { order_distributor_.onAccountInfo(upd_); }

  void PublishAccountOrder(const coin::proto::OrderEvent& order_event) {
    if (!upd_.product_order_info().event().duplicate()) {
      order_distributor_.onAccountOrder(upd_.native_symbol(), upd_);
    }
  }

  void SetOrderManager(const OrderContextManager* order_manager) {
    ocm_ = const_cast<OrderContextManager*>(order_manager);
  }

  const OrderContextManager* order_manager() const { return ocm_; }

 protected:
  IPacketSubscriber* packet_subscriber_;
  OrderDistributor order_distributor_;

  OrderUpdate upd_;
  AccountInfo account_info_;
  std::map<std::string, std::unique_ptr<ProductOrderInfo>> product_infos_;
  std::map<std::string, int64_t> fill_pos_init_ts_map_;
  OrderContextManager* ocm_{nullptr};
};

}  // namespace coin2::exchange::base::order
